/*
 *      Xing VBR tagging for LAME.
 *
 *      Copyright (c) 1999 A.L. Faber
 *      Copyright (c) 2001 Jonathan Dee
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
package net.sourceforge.lame.mp3;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * A Vbr header may be present in the ancillary data field of the first frame of
 * an mp3 bitstream<BR>
 * The Vbr header (optionally) contains
 * <UL>
 * <LI>frames total number of audio frames in the bitstream
 * <LI>bytes total number of bytes in the bitstream
 * <LI>toc table of contents
 * </UL>
 * <p/>
 * toc (table of contents) gives seek points for random access.<BR>
 * The ith entry determines the seek point for i-percent duration.<BR>
 * seek point in bytes = (toc[i]/256.0) * total_bitstream_bytes<BR>
 * e.g. half duration seek point = (toc[50]/256.0) * total_bitstream_bytes
 */
public class VBRTag {

  public static final int NUMTOCENTRIES = 100;
  /**
   * <PRE>
   * 4 bytes for Header Tag
   * 4 bytes for Header Flags
   * 100 bytes for entry (toc)
   * 4 bytes for frame size
   * 4 bytes for stream size
   * 4 bytes for VBR scale. a VBR quality indicator: 0=best 100=worst
   * 20 bytes for LAME tag.  for example, "LAME3.12 (beta 6)"
   * ___________
   * 140 bytes
   * </PRE>
   */
  private static final int VBRHEADERSIZE = (NUMTOCENTRIES + 4 + 4 + 4 + 4 + 4);
  private static final int LAMEHEADERSIZE = (VBRHEADERSIZE + 9 + 1 + 1 + 8
      + 1 + 1 + 3 + 1 + 1 + 2 + 4 + 2 + 2);
  /**
   * (0xB40) the max freeformat 640 32kHz framesize.
   */
  public static final int MAXFRAMESIZE = 2880;
  private static final int FRAMES_FLAG = 0x0001;
  private static final int BYTES_FLAG = 0x0002;
  private static final int TOC_FLAG = 0x0004;
  private static final int VBR_SCALE_FLAG = 0x0008;
  /**
   * The size of the Xing header MPEG-1, bit rate in kbps.
   */
  private static final int XING_BITRATE1 = 128;
  /**
   * The size of the Xing header MPEG-2, bit rate in kbps.
   */
  private static final int XING_BITRATE2 = 64;
  /**
   * The size of the Xing header MPEG-2.5, bit rate in kbps.
   */
  private static final int XING_BITRATE25 = 32;
  /**
   * ISO-8859-1 charset for byte to string operations.
   */
  private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
  /**
   * VBR header magic string.
   */
  private static final String VBRTag0 = "Xing";
  /**
   * VBR header magic string (VBR == VBRMode.vbr_off).
   */
  private static final String VBRTag1 = "Info";
  /**
   * Lookup table for fast CRC-16 computation. Uses the polynomial
   * x^16+x^15+x^2+1
   */
  private static int crc16Lookup[] = {0x0000, 0xC0C1, 0xC181, 0x0140,
      0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741,
      0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41,
      0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40,
      0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941,
      0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40,
      0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540,
      0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341,
      0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141,
      0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740,
      0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40,
      0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41,
      0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940,
      0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41,
      0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541,
      0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340,
      0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141,
      0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740,
      0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40,
      0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41,
      0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940,
      0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41,
      0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541,
      0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340,
      0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140,
      0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741,
      0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41,
      0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40,
      0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941,
      0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40,
      0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540,
      0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341,
      0x4100, 0x81C1, 0x8081, 0x4040};
  Lame lame;
  BitStream bs;
  private Version version = new Version();

  public final void setModules(Lame lame, BitStream bs) {
    this.lame = lame;
    this.bs = bs;
  }

  /**
   * ********************************************************************
   * Robert Hegemann 2001-01-17
   * *********************************************************************
   */

  private void addVbr(final VBRSeekInfo v, final int bitrate) {
    v.nVbrNumFrames++;
    v.sum += bitrate;
    v.seen++;

    if (v.seen < v.want) {
      return;
    }

    if (v.pos < v.size) {
      v.bag[v.pos] = v.sum;
      v.pos++;
      v.seen = 0;
    }
    if (v.pos == v.size) {
      for (int i = 1; i < v.size; i += 2) {
        v.bag[i / 2] = v.bag[i];
      }
      v.want *= 2;
      v.pos /= 2;
    }
  }

  private void xingSeekTable(VBRSeekInfo v, byte[] t) {
    if (v.pos <= 0)
      return;

    for (int i = 1; i < NUMTOCENTRIES; ++i) {
      float j = i / (float) NUMTOCENTRIES, act, sum;
      int indx = (int) (Math.floor(j * v.pos));
      if (indx > v.pos - 1)
        indx = v.pos - 1;
      act = v.bag[indx];
      sum = v.sum;
      int seek_point = (int) (256. * act / sum);
      if (seek_point > 255)
        seek_point = 255;
      t[i] = (byte) seek_point;
    }
  }

  /**
   * Add VBR entry, used to fill the VBR TOC entries.
   *
   * @param gfp global flags
   */
  public final void addVbrFrame(final LameGlobalFlags gfp) {
    final LameInternalFlags gfc = gfp.internal_flags;

    int kbps = Tables.bitrate_table[gfp.getMpegVersion()][gfc.bitrate_index];
    assert (gfc.VBR_seek_table.bag != null);
    addVbr(gfc.VBR_seek_table, kbps);
  }

  /**
   * Read big endian integer (4-bytes) from header.
   *
   * @param buf    header containing the integer
   * @param bufPos offset into the header
   * @return extracted integer
   */
  private int extractInteger(final byte[] buf, final int bufPos) {
    int x = buf[bufPos + 0] & 0xff;
    x <<= 8;
    x |= buf[bufPos + 1] & 0xff;
    x <<= 8;
    x |= buf[bufPos + 2] & 0xff;
    x <<= 8;
    x |= buf[bufPos + 3] & 0xff;
    return x;
  }

  /**
   * Write big endian integer (4-bytes) in the header.
   *
   * @param buf    header to write the integer into
   * @param bufPos offset into the header
   * @param value  integer value to write
   */
  private void createInteger(final byte[] buf, final int bufPos,
                             final int value) {
    buf[bufPos + 0] = (byte) ((value >> 24) & 0xff);
    buf[bufPos + 1] = (byte) ((value >> 16) & 0xff);
    buf[bufPos + 2] = (byte) ((value >> 8) & 0xff);
    buf[bufPos + 3] = (byte) (value & 0xff);
  }

  /**
   * Write big endian short (2-bytes) in the header.
   *
   * @param buf    header to write the integer into
   * @param bufPos offset into the header
   * @param value  integer value to write
   */
  private void createShort(final byte[] buf, final int bufPos, final int value) {
    buf[bufPos + 0] = (byte) ((value >> 8) & 0xff);
    buf[bufPos + 1] = (byte) (value & 0xff);
  }

  /**
   * Check for magic strings (Xing/Info).
   *
   * @param buf    header to check
   * @param bufPos header offset to check
   * @return magic string found
   */
  private boolean isVbrTag(final byte[] buf, final int bufPos) {
    return new String(buf, bufPos, VBRTag0.length(), ISO_8859_1)
        .equals(VBRTag0)
        || new String(buf, bufPos, VBRTag1.length(), ISO_8859_1)
        .equals(VBRTag1);
  }

  private byte shiftInBitsValue(final byte x, final int n, final int v) {
    return (byte) ((x << n) | (v & ~(-1 << n)));
  }

  /**
   * Construct the MP3 header using the settings of the global flags.
   * <p/>
   * <img src="1000px-Mp3filestructure.svg.png">
   *
   * @param gfp    global flags
   * @param buffer header
   */
  private void setLameTagFrameHeader(final LameGlobalFlags gfp,
                                     final byte[] buffer) {
    final LameInternalFlags gfc = gfp.internal_flags;

    // MP3 Sync Word
    buffer[0] = shiftInBitsValue(buffer[0], 8, 0xff);

    buffer[1] = shiftInBitsValue(buffer[1], 3, 7);
    buffer[1] = shiftInBitsValue(buffer[1], 1,
        (gfp.getOutSampleRate() < 16000) ? 0 : 1);
    // Version
    buffer[1] = shiftInBitsValue(buffer[1], 1, gfp.getMpegVersion());
    // 01 == Layer 3
    buffer[1] = shiftInBitsValue(buffer[1], 2, 4 - 3);
    // Error protection
    buffer[1] = shiftInBitsValue(buffer[1], 1, (!gfp.error_protection) ? 1
        : 0);

    // Bit rate
    buffer[2] = shiftInBitsValue(buffer[2], 4, gfc.bitrate_index);
    // Frequency
    buffer[2] = shiftInBitsValue(buffer[2], 2, gfc.samplerate_index);
    // Pad. Bit
    buffer[2] = shiftInBitsValue(buffer[2], 1, 0);
    // Priv. Bit
    buffer[2] = shiftInBitsValue(buffer[2], 1, gfp.extension);

    // Mode
    buffer[3] = shiftInBitsValue(buffer[3], 2, gfp.getMode().getNumMode());
    // Mode extension (Used with Joint Stereo)
    buffer[3] = shiftInBitsValue(buffer[3], 2, gfc.mode_ext);
    // Copy
    buffer[3] = shiftInBitsValue(buffer[3], 1, gfp.copyright);
    // Original
    buffer[3] = shiftInBitsValue(buffer[3], 1, gfp.original);
    // Emphasis
    buffer[3] = shiftInBitsValue(buffer[3], 2, gfp.emphasis);

		/* the default VBR header. 48 kbps layer III, no padding, no crc */
    /* but sampling freq, mode and copyright/copy protection taken */
		/* from first valid frame */
    buffer[0] = (byte) 0xff;
    byte abyte = (byte) (buffer[1] & 0xf1);

    int bitrate;
    if (1 == gfp.getMpegVersion()) {
      bitrate = XING_BITRATE1;
    } else {
      if (gfp.getOutSampleRate() < 16000)
        bitrate = XING_BITRATE25;
      else
        bitrate = XING_BITRATE2;
    }

    if (gfp.getVBR() == VbrMode.vbr_off)
      bitrate = gfp.getBitRate();

    byte bbyte;
    if (gfp.free_format)
      bbyte = 0x00;
    else
      bbyte = (byte) (16 * lame.BitrateIndex(bitrate, gfp.getMpegVersion(),
          gfp.getOutSampleRate()));

		/*
		 * Use as much of the info from the real frames in the Xing header:
		 * samplerate, channels, crc, etc...
		 */
    if (gfp.getMpegVersion() == 1) {
			/* MPEG1 */
      buffer[1] = (byte) (abyte | 0x0a); /* was 0x0b; */
      abyte = (byte) (buffer[2] & 0x0d); /* AF keep also private bit */
      buffer[2] = (byte) (bbyte | abyte); /* 64kbs MPEG1 frame */
    } else {
			/* MPEG2 */
      buffer[1] = (byte) (abyte | 0x02); /* was 0x03; */
      abyte = (byte) (buffer[2] & 0x0d); /* AF keep also private bit */
      buffer[2] = (byte) (bbyte | abyte); /* 64kbs MPEG2 frame */
    }
  }

  /**
   * Get VBR tag information
   *
   * @param buf    header to analyze
   * @param bufPos offset into the header
   * @return VBR tag data
   */
  public final VBRTagData getVbrTag(final byte[] buf) {
    final VBRTagData pTagData = new VBRTagData();
    int bufPos = 0;
		
		/* get Vbr header data */
    pTagData.flags = 0;

		/* get selected MPEG header data */
    int hId = (buf[bufPos + 1] >> 3) & 1;
    int hSrIndex = (buf[bufPos + 2] >> 2) & 3;
    int hMode = (buf[bufPos + 3] >> 6) & 3;
    int hBitrate = ((buf[bufPos + 2] >> 4) & 0xf);
    hBitrate = Tables.bitrate_table[hId][hBitrate];

		/* check for FFE syncword */
    if ((buf[bufPos + 1] >> 4) == 0xE)
      pTagData.samprate = Tables.samplerate_table[2][hSrIndex];
    else
      pTagData.samprate = Tables.samplerate_table[hId][hSrIndex];

		/* determine offset of header */
    if (hId != 0) {
			/* mpeg1 */
      if (hMode != 3)
        bufPos += (32 + 4);
      else
        bufPos += (17 + 4);
    } else {
			/* mpeg2 */
      if (hMode != 3)
        bufPos += (17 + 4);
      else
        bufPos += (9 + 4);
    }

    if (!isVbrTag(buf, bufPos))
      return null;

    bufPos += 4;

    pTagData.hId = hId;

		/* get flags */
    int head_flags = pTagData.flags = extractInteger(buf, bufPos);
    bufPos += 4;

    if ((head_flags & FRAMES_FLAG) != 0) {
      pTagData.frames = extractInteger(buf, bufPos);
      bufPos += 4;
    }

    if ((head_flags & BYTES_FLAG) != 0) {
      pTagData.bytes = extractInteger(buf, bufPos);
      bufPos += 4;
    }

    if ((head_flags & TOC_FLAG) != 0) {
      if (pTagData.toc != null) {
        for (int i = 0; i < NUMTOCENTRIES; i++)
          pTagData.toc[i] = buf[bufPos + i];
      }
      bufPos += NUMTOCENTRIES;
    }

    pTagData.vbrScale = -1;

    if ((head_flags & VBR_SCALE_FLAG) != 0) {
      pTagData.vbrScale = extractInteger(buf, bufPos);
      bufPos += 4;
    }

    pTagData.headersize = ((hId + 1) * 72000 * hBitrate)
        / pTagData.samprate;

    bufPos += 21;
    int encDelay = buf[bufPos + 0] << 4;
    encDelay += buf[bufPos + 1] >> 4;
    int encPadding = (buf[bufPos + 1] & 0x0F) << 8;
    encPadding += buf[bufPos + 2] & 0xff;
		/* check for reasonable values (this may be an old Xing header, */
		/* not a INFO tag) */
    if (encDelay < 0 || encDelay > 3000)
      encDelay = -1;
    if (encPadding < 0 || encPadding > 3000)
      encPadding = -1;

    pTagData.encDelay = encDelay;
    pTagData.encPadding = encPadding;

		/* success */
    return pTagData;
  }

  /**
   * Initializes the header
   *
   * @param gfp global flags
   */
  public final void InitVbrTag(final LameGlobalFlags gfp) {
    final LameInternalFlags gfc = gfp.internal_flags;

    /**
     * <PRE>
     * Xing VBR pretends to be a 48kbs layer III frame.  (at 44.1kHz).
     * (at 48kHz they use 56kbs since 48kbs frame not big enough for
     * table of contents)
     * let's always embed Xing header inside a 64kbs layer III frame.
     * this gives us enough room for a LAME version string too.
     * size determined by sampling frequency (MPEG1)
     * 32kHz:    216 bytes@48kbs    288bytes@ 64kbs
     * 44.1kHz:  156 bytes          208bytes@64kbs     (+1 if padding = 1)
     * 48kHz:    144 bytes          192
     *
     * MPEG 2 values are the same since the framesize and samplerate
     * are each reduced by a factor of 2.
     * </PRE>
     */
    int kbps_header;
    if (1 == gfp.getMpegVersion()) {
      kbps_header = XING_BITRATE1;
    } else {
      if (gfp.getOutSampleRate() < 16000)
        kbps_header = XING_BITRATE25;
      else
        kbps_header = XING_BITRATE2;
    }

    if (gfp.getVBR() == VbrMode.vbr_off)
      kbps_header = gfp.getBitRate();

    // make sure LAME Header fits into Frame
    int totalFrameSize = ((gfp.getMpegVersion() + 1) * 72000 * kbps_header)
        / gfp.getOutSampleRate();
    int headerSize = (gfc.sideinfo_len + LAMEHEADERSIZE);
    gfc.VBR_seek_table.TotalFrameSize = totalFrameSize;
    if (totalFrameSize < headerSize || totalFrameSize > MAXFRAMESIZE) {
			/* disable tag, it wont fit */
      gfp.bWriteVbrTag = false;
      return;
    }

    gfc.VBR_seek_table.nVbrNumFrames = 0;
    gfc.VBR_seek_table.nBytesWritten = 0;
    gfc.VBR_seek_table.sum = 0;

    gfc.VBR_seek_table.seen = 0;
    gfc.VBR_seek_table.want = 1;
    gfc.VBR_seek_table.pos = 0;

    if (gfc.VBR_seek_table.bag == null) {
      gfc.VBR_seek_table.bag = new int[400];
      gfc.VBR_seek_table.size = 400;
    }

    // write dummy VBR tag of all 0's into bitstream
    byte buffer[] = new byte[MAXFRAMESIZE];

    setLameTagFrameHeader(gfp, buffer);
    int n = gfc.VBR_seek_table.TotalFrameSize;
    for (int i = 0; i < n; ++i) {
      bs.add_dummy_byte(gfp, buffer[i] & 0xff, 1);
    }
  }

  /**
   * Fast CRC-16 computation (uses table crc16Lookup).
   *
   * @param value
   * @param crc
   * @return
   */
  private int crcUpdateLookup(final int value, int crc) {
    int tmp = crc ^ value;
    crc = (crc >> 8) ^ crc16Lookup[tmp & 0xff];
    return crc;
  }

  public final void updateMusicCRC(final int[] crc, final byte[] buffer,
                                   final int bufferPos, final int size) {
    for (int i = 0; i < size; ++i)
      crc[0] = crcUpdateLookup(buffer[bufferPos + i], crc[0]);
  }

  /**
   * Write LAME info: mini version + info on various switches used (Jonathan
   * Dee 2001/08/31).
   *
   * @param gfp             global flags
   * @param musicLength     music length
   * @param streamBuffer    pointer to output buffer
   * @param streamBufferPos offset into the output buffer
   * @param crc             computation of CRC-16 of Lame Tag so far (starting at frame
   *                        sync)
   * @return number of bytes written to the stream
   */
  private int putLameVBR(final LameGlobalFlags gfp, final int musicLength,
                         final byte[] streamBuffer, final int streamBufferPos, int crc) {
    final LameInternalFlags gfc = gfp.internal_flags;

    int bytesWritten = 0;

		/* encoder delay */
    int encDelay = gfp.getEncoderDelay();
		/* encoder padding */
    int encPadding = gfp.encoder_padding;

		/* recall: gfp.VBR_q is for example set by the switch -V */
		/* gfp.quality by -q, -h, -f, etc */
    int quality = (100 - 10 * gfp.getVBRQuality() - gfp.getQuality());

    final String versionStr = version.getLameVeryShortVersion();
    int vbr;
    int revision = 0x00;
    int revMethod;
    // numbering different in vbr_mode vs. Lame tag
    int vbrTypeTranslator[] = {1, 5, 3, 2, 4, 0, 3};

    int lowpass = (int) (((gfp.lowpassfreq / 100.0) + .5) > 255 ? 255
        : (gfp.lowpassfreq / 100.0) + .5);

    int peakSignalAmplitude = 0;

    int radioReplayGain = 0;
    int audiophileReplayGain = 0;

    int noiseShaping = gfp.internal_flags.noise_shaping;
    int stereoMode = 0;
    int nonOptimal = 0;
    int sourceFreq = 0;
    int misc = 0;
    int musicCRC = 0;

    // psy model type: Gpsycho or NsPsytune
    boolean expNPsyTune = (gfp.exp_nspsytune & 1) != 0;
    boolean safeJoint = (gfp.exp_nspsytune & 2) != 0;

    boolean noGapMore = false;
    boolean noGapPrevious = false;

    int noGapCount = gfp.internal_flags.nogap_total;
    int noGapCurr = gfp.internal_flags.nogap_current;

    // 4 bits
    int athType = gfp.ATHtype;

    int flags = 0;

    // vbr modes
    int abrBitrate;
    switch (gfp.getVBR()) {
      case vbr_abr:
        abrBitrate = gfp.VBR_mean_bitrate_kbps;
        break;
      case vbr_off:
        abrBitrate = gfp.getBitRate();
        break;
      default:
        abrBitrate = gfp.VBR_min_bitrate_kbps;
    }

    // revision and vbr method
    if (gfp.getVBR().ordinal() < vbrTypeTranslator.length)
      vbr = vbrTypeTranslator[gfp.getVBR().ordinal()];
    else
      vbr = 0x00; // unknown

    revMethod = 0x10 * revision + vbr;

    // ReplayGain
    if (gfc.findReplayGain) {
      if (gfc.RadioGain > 0x1FE)
        gfc.RadioGain = 0x1FE;
      if (gfc.RadioGain < -0x1FE)
        gfc.RadioGain = -0x1FE;

      // set name code
      radioReplayGain = 0x2000;
      // set originator code to `determined automatically'
      radioReplayGain |= 0xC00;

      if (gfc.RadioGain >= 0) {
        // set gain adjustment
        radioReplayGain |= gfc.RadioGain;
      } else {
        // set the sign bit
        radioReplayGain |= 0x200;
        // set gain adjustment
        radioReplayGain |= -gfc.RadioGain;
      }
    }

    // peak sample
    if (gfc.findPeakSample)
      peakSignalAmplitude = Math
          .abs((int) ((((float) gfc.PeakSample) / 32767.0)
              * Math.pow(2, 23) + .5));

    // nogap
    if (noGapCount != -1) {
      if (noGapCurr > 0)
        noGapPrevious = true;

      if (noGapCurr < noGapCount - 1)
        noGapMore = true;
    }

    // flags
    flags = athType + ((expNPsyTune ? 1 : 0) << 4)
        + ((safeJoint ? 1 : 0) << 5) + ((noGapMore ? 1 : 0) << 6)
        + ((noGapPrevious ? 1 : 0) << 7);

    if (quality < 0)
      quality = 0;

    // stereo mode field (Intensity stereo is not implemented)
    switch (gfp.getMode()) {
      case MONO:
        stereoMode = 0;
        break;
      case STEREO:
        stereoMode = 1;
        break;
      case DUAL_CHANNEL:
        stereoMode = 2;
        break;
      case JOINT_STEREO:
        if (gfp.force_ms)
          stereoMode = 4;
        else
          stereoMode = 3;
        break;
      case NOT_SET:
        //$FALL-THROUGH$
      default:
        stereoMode = 7;
        break;
    }

    if (gfp.getInSampleRate() <= 32000)
      sourceFreq = 0x00;
    else if (gfp.getInSampleRate() == 48000)
      sourceFreq = 0x02;
    else if (gfp.getInSampleRate() > 48000)
      sourceFreq = 0x03;
    else {
      // default is 44100Hz
      sourceFreq = 0x01;
    }

    // Check if the user overrided the default LAME behavior with some
    // nasty options
    if (gfp.short_blocks == ShortBlock.short_block_forced
        || gfp.short_blocks == ShortBlock.short_block_dispensed
        || ((gfp.lowpassfreq == -1) && (gfp.highpassfreq == -1)) || /* "-k" */
        (gfp.scale_left < gfp.scale_right)
        || (gfp.scale_left > gfp.scale_right)
        || (gfp.disable_reservoir && gfp.getBitRate() < 320) || gfp.noATH
        || gfp.ATHonly || (athType == 0) || gfp.getInSampleRate() <= 32000)
      nonOptimal = 1;

    misc = noiseShaping + (stereoMode << 2) + (nonOptimal << 5)
        + (sourceFreq << 6);

    musicCRC = gfc.nMusicCRC;

    // Write all this information into the stream

    createInteger(streamBuffer, streamBufferPos + bytesWritten, quality);
    bytesWritten += 4;

    for (int j = 0; j < 9; j++) {
      streamBuffer[streamBufferPos + bytesWritten + j] = (byte) versionStr
          .charAt(j);
    }
    bytesWritten += 9;

    streamBuffer[streamBufferPos + bytesWritten] = (byte) revMethod;
    bytesWritten++;

    streamBuffer[streamBufferPos + bytesWritten] = (byte) lowpass;
    bytesWritten++;

    createInteger(streamBuffer, streamBufferPos + bytesWritten,
        peakSignalAmplitude);
    bytesWritten += 4;

    createShort(streamBuffer, streamBufferPos + bytesWritten,
        radioReplayGain);
    bytesWritten += 2;

    createShort(streamBuffer, streamBufferPos + bytesWritten,
        audiophileReplayGain);
    bytesWritten += 2;

    streamBuffer[streamBufferPos + bytesWritten] = (byte) flags;
    bytesWritten++;

    if (abrBitrate >= 255)
      streamBuffer[streamBufferPos + bytesWritten] = (byte) 0xFF;
    else
      streamBuffer[streamBufferPos + bytesWritten] = (byte) abrBitrate;
    bytesWritten++;

    streamBuffer[streamBufferPos + bytesWritten] = (byte) (encDelay >> 4);
    streamBuffer[streamBufferPos + bytesWritten + 1] = (byte) ((encDelay << 4) + (encPadding >> 8));
    streamBuffer[streamBufferPos + bytesWritten + 2] = (byte) encPadding;

    bytesWritten += 3;

    streamBuffer[streamBufferPos + bytesWritten] = (byte) misc;
    bytesWritten++;

    // unused in rev0
    streamBuffer[streamBufferPos + bytesWritten++] = 0;

    createShort(streamBuffer, streamBufferPos + bytesWritten, gfp.preset);
    bytesWritten += 2;

    createInteger(streamBuffer, streamBufferPos + bytesWritten, musicLength);
    bytesWritten += 4;

    createShort(streamBuffer, streamBufferPos + bytesWritten, musicCRC);
    bytesWritten += 2;

    // Calculate tag CRC.... must be done here, since it includes previous
    // information

    for (int i = 0; i < bytesWritten; i++)
      crc = crcUpdateLookup(streamBuffer[streamBufferPos + i], crc);

    createShort(streamBuffer, streamBufferPos + bytesWritten, crc);
    bytesWritten += 2;

    return bytesWritten;
  }

  private int skipId3v2(final RandomAccessFile fpStream) throws IOException {
    // seek to the beginning of the stream
    fpStream.seek(0);
    // read 10 bytes in case there's an ID3 version 2 header here
    byte[] id3v2Header = new byte[10];
    fpStream.readFully(id3v2Header);
		/* does the stream begin with the ID3 version 2 file identifier? */
    int id3v2TagSize;
    if (!new String(id3v2Header, "ISO-8859-1").startsWith("ID3")) {
			/*
			 * the tag size (minus the 10-byte header) is encoded into four
			 * bytes where the most significant bit is clear in each byte
			 */
      id3v2TagSize = (((id3v2Header[6] & 0x7f) << 21)
          | ((id3v2Header[7] & 0x7f) << 14)
          | ((id3v2Header[8] & 0x7f) << 7) | (id3v2Header[9] & 0x7f))
          + id3v2Header.length;
    } else {
			/* no ID3 version 2 tag in this stream */
      id3v2TagSize = 0;
    }
    return id3v2TagSize;
  }

  public final int getLameTagFrame(final LameGlobalFlags gfp,
                                   final byte[] buffer) {
    final LameInternalFlags gfc = gfp.internal_flags;

    if (!gfp.bWriteVbrTag) {
      return 0;
    }
    if (gfc.Class_ID != Lame.LAME_ID) {
      return 0;
    }
    if (gfc.VBR_seek_table.pos <= 0) {
      return 0;
    }
    if (buffer.length < gfc.VBR_seek_table.TotalFrameSize) {
      return gfc.VBR_seek_table.TotalFrameSize;
    }

    Arrays.fill(buffer, 0, gfc.VBR_seek_table.TotalFrameSize, (byte) 0);

    // 4 bytes frame header
    setLameTagFrameHeader(gfp, buffer);

    // Create TOC entries
    byte toc[] = new byte[NUMTOCENTRIES];

    if (gfp.free_format) {
      for (int i = 1; i < NUMTOCENTRIES; ++i)
        toc[i] = (byte) (255 * i / 100);
    } else {
      xingSeekTable(gfc.VBR_seek_table, toc);
    }

    // Start writing the tag after the zero frame
    int streamIndex = gfc.sideinfo_len;
    /**
     * Note: Xing header specifies that Xing data goes in the ancillary data
     * with NO ERROR PROTECTION. If error protecton in enabled, the Xing
     * data still starts at the same offset, and now it is in sideinfo data
     * block, and thus will not decode correctly by non-Xing tag aware
     * players
     */
    if (gfp.error_protection)
      streamIndex -= 2;

    // Put Vbr tag
    if (gfp.getVBR() == VbrMode.vbr_off) {
      buffer[streamIndex++] = (byte) VBRTag1.charAt(0);
      buffer[streamIndex++] = (byte) VBRTag1.charAt(1);
      buffer[streamIndex++] = (byte) VBRTag1.charAt(2);
      buffer[streamIndex++] = (byte) VBRTag1.charAt(3);

    } else {
      buffer[streamIndex++] = (byte) VBRTag0.charAt(0);
      buffer[streamIndex++] = (byte) VBRTag0.charAt(1);
      buffer[streamIndex++] = (byte) VBRTag0.charAt(2);
      buffer[streamIndex++] = (byte) VBRTag0.charAt(3);
    }

    // Put header flags
    createInteger(buffer, streamIndex, FRAMES_FLAG + BYTES_FLAG + TOC_FLAG
        + VBR_SCALE_FLAG);
    streamIndex += 4;

    // Put Total Number of frames
    createInteger(buffer, streamIndex, gfc.VBR_seek_table.nVbrNumFrames);
    streamIndex += 4;

    // Put total audio stream size, including Xing/LAME Header
    int streamSize = (gfc.VBR_seek_table.nBytesWritten + gfc.VBR_seek_table.TotalFrameSize);
    createInteger(buffer, streamIndex, (int) streamSize);
    streamIndex += 4;

		/* Put TOC */
    System.arraycopy(toc, 0, buffer, streamIndex, toc.length);
    streamIndex += toc.length;

    if (gfp.error_protection) {
      // (jo) error_protection: add crc16 information to header
      bs.CRC_writeheader(gfc, buffer);
    }

    // work out CRC so far: initially crc = 0
    int crc = 0x00;
    for (int i = 0; i < streamIndex; i++)
      crc = crcUpdateLookup(buffer[i], crc);
    // Put LAME VBR info
    streamIndex += putLameVBR(gfp, streamSize, buffer, streamIndex, crc);

    return gfc.VBR_seek_table.TotalFrameSize;
  }

  /**
   * Write final VBR tag to the file.
   *
   * @param gfp    global flags
   * @param stream stream to add the VBR tag to
   * @return 0 (OK), -1 else
   * @throws java.io.IOException I/O error
   */
  public final int putVbrTag(final LameGlobalFlags gfp,
                             final RandomAccessFile stream) throws IOException {
    final LameInternalFlags gfc = gfp.internal_flags;

    if (gfc.VBR_seek_table.pos <= 0)
      return -1;

    // Seek to end of file
    stream.seek(stream.length());

    // Get file size, abort if file has zero length.
    if (stream.length() == 0)
      return -1;

    // The VBR tag may NOT be located at the beginning of the stream. If an
    // ID3 version 2 tag was added, then it must be skipped to write the VBR
    // tag data.
    int id3v2TagSize = skipId3v2(stream);

    // Seek to the beginning of the stream
    stream.seek(id3v2TagSize);

    byte[] buffer = new byte[MAXFRAMESIZE];
    int bytes = getLameTagFrame(gfp, buffer);
    if (bytes > buffer.length) {
      return -1;
    }

    if (bytes < 1) {
      return 0;
    }

    // Put it all to disk again
    stream.write(buffer, 0, bytes);
    // success
    return 0;
  }

}
